package com.cqcis.uip.platform.launcher;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.packageadmin.PackageAdmin;

public class DirectoryWatcher extends Thread {
	public final static String LAUNCHER_POLL_KEY = "uip.launcher.poll";
	public final static String LAUNCHER_DIRS_KEY = "uip.launcher.dirs";

	private final static String ALIAS_KEY = "_alias_factory_pid";
	private final static String LAUNCHER_DIRS = "libs";
	private long poll = 4000; // 4s

	private File[] dirs;
	private boolean polling = true;

	private Map<String, Long> oldBundlesLastModify = new LinkedHashMap<String, Long>();
	private Map<String, Long> oldBundles = new LinkedHashMap<String, Long>();
	private Map<String, Long> oldConfigs = new LinkedHashMap<String, Long>();
	private Map<String, File> curBundles = new LinkedHashMap<String, File>();
	private Map<String, File> curConfigs = new LinkedHashMap<String, File>();

	private BundleContext context;
	private boolean reported;
	
	/**
	 * Construct function
	 * 
	 * @param properties
	 * @param context
	 */
	@SuppressWarnings("unchecked")
	public DirectoryWatcher(Dictionary properties, BundleContext context) {
		super("DirectoryWatcher");
		this.context = context;		
		poll = getLong(LAUNCHER_POLL_KEY, poll);

		String dirStr = (String) properties.get(LAUNCHER_DIRS_KEY);
		if (dirStr == null)
			dirStr = LAUNCHER_DIRS;

		String[] ds = dirStr.split(",");
		dirs = new File[ds.length];
		for (int i = 0; i < ds.length; i++) {
			File f = new File(ds[i]);
			f.mkdirs();
			dirs[i] = f;
		}
	}

	/**
	 * @see java.lang.Thread#run()
	 */
	public void run() {
		while (polling) {
			try {
				watcher();
				Thread.sleep(poll);
			} catch (InterruptedException e) {
			} catch (Throwable e) {
				log("In main loop, we have serious trouble", e);
			}
		}
	}

	/**
	 * Directory watcher
	 * @throws Exception 
	 */
	public void watcher() throws Exception {
		for (int i = 0; i < dirs.length; i++) {
			traverse(curBundles, curConfigs, dirs[i]);
		}

		doBundles(oldBundles, oldBundlesLastModify, curBundles);
		doConfigs(oldConfigs, curConfigs);
		
		curBundles.clear();
		curConfigs.clear();
	}
	
	/**
	 * Get property long
	 */
	private long getLong(String property, long init) {
		String value = context.getProperty(property);
		if (value != null) {
			try {
				return Long.parseLong(value);
			} catch (Exception e) {
				log(property + " set, but not a long: " + value);
			}
		}
		return init;
	}

	/**
	 * @param olds
	 * @param currs
	 */
	private void doConfigs(Map<String, Long> olds, Map<String, File> currs) {
		try {
			Set<String> oldKeys = new HashSet<String>(olds.keySet());
			for (Iterator<Map.Entry<String, File>> e = currs.entrySet().iterator(); e.hasNext();) {
				Map.Entry<String, File> entry = (Map.Entry<String, File>) e.next();
				String path = (String) entry.getKey();
				File f = (File) entry.getValue();
				if (!oldKeys.contains(path)) {
					// new
					Long l = new Long(f.lastModified());
					if (setConfig(f))
						olds.put(path, l);
				} else {
					long lastModified = f.lastModified();
					long oldTime = ((Long) olds.get(path)).longValue();
					if (oldTime < lastModified) {
						if (setConfig(f))
							olds.put(path, new Long(lastModified));
					}
				}
				oldKeys.remove(path);
			}
			
			for (Iterator<String> e = oldKeys.iterator(); e.hasNext();) {
				String path = (String) e.next();
				File f = new File(path);
				if (deleteConfig(f))
					oldConfigs.remove(path);
			}
		} catch (Exception ee) {
			log("Processing config: ", ee);
		}
	}

	/**
	 * @param f
	 * @return
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	private boolean setConfig(File f) throws Exception {
		ConfigurationAdmin cm = (ConfigurationAdmin) BundleAutoDeploy.cmTracker.getService();
		if (cm == null) {
			if (!reported) {
				System.err.println("Can't find a Configuration Manager, configurations do not work");
				reported = true;
			}
			return false;
		}

		Properties p = new Properties();
		InputStream in = new FileInputStream(f);
		p.load(in);
		in.close();
		String pid[] = parsePid(f.getName());
		Hashtable ht = new Hashtable();
		ht.putAll(p);
		if (pid[1] != null)
			ht.put(ALIAS_KEY, pid[1]);

		Configuration config = getConfiguration(pid[0], pid[1]);
		if (config.getBundleLocation() != null)
			config.setBundleLocation(null);
		config.update(ht);
		return true;
	}

	/**
	 * @param f
	 * @return
	 * @throws Exception
	 */
	private boolean deleteConfig(File f) throws Exception {
		String pid[] = parsePid(f.getName());
		Configuration config = getConfiguration(pid[0], pid[1]);
		config.delete();
		return true;
	}

	/**
	 * @param path
	 * @return
	 */
	private String[] parsePid(String path) {
		String pid = path.substring(0, path.length() - 4);
		int n = pid.indexOf('-');
		if (n > 0) {
			String factoryPid = pid.substring(n + 1);
			pid = pid.substring(0, n);
			return new String[] { pid, factoryPid };
		} else
			return new String[] { pid, null };
	}

	/**
	 * @param pid
	 * @param factoryPid
	 * @return
	 * @throws Exception
	 */
	private Configuration getConfiguration(String pid, String factoryPid) throws Exception {
		ConfigurationAdmin cm = (ConfigurationAdmin) BundleAutoDeploy.cmTracker.getService();
		if (factoryPid != null) {
			Configuration configs[] = cm.listConfigurations("(|(" + ALIAS_KEY + "=" + factoryPid
					+ ")(.alias_factory_pid=" + factoryPid + "))");
			if (configs == null || configs.length == 0)
				return cm.createFactoryConfiguration(pid, null);
			else
				return configs[0];
		} else
			return cm.getConfiguration(pid, null);
	}

	/**
	 * Install jar
	 * 
	 * @param olds
	 * @param news
	 */
	private void doBundles(Map<String, Long> olds, Map<String, Long> oldBundlesLastModify, Map<String, File> currs) throws Exception {
		List<Bundle> notStarts = new ArrayList<Bundle>();
		List<Bundle> starts = new ArrayList<Bundle>();
		boolean refresh = false;
		Bundle bundles[] = context.getBundles();
		for (int i = 0; i < bundles.length; i++) {
			Bundle bundle = bundles[i];
			String location = bundle.getLocation();
			File file = (File) currs.get(location);
			if (file != null) {
				// Modified date does not work on the Nokia for some reason, so we take size into account as well.
				long newSize = file.length();
				Long oldSizeObj = (Long) olds.get(location);
				long oldSize = oldSizeObj == null ? 0 : oldSizeObj.longValue();
				Long bundleLastMotify = oldBundlesLastModify.get(location);

				currs.remove(location);
				
				if (file.lastModified() > bundleLastMotify + poll || oldSize != newSize) {
					try {				
						olds.put(location, new Long(newSize));
						oldBundlesLastModify.put(location, file.lastModified());
						InputStream in = new FileInputStream(file);
						bundle.update(in);
						refresh = true;
						in.close();
						
						if (!isFragment(bundle)) {
							starts.add(bundle);
						}

						log("Updated " + location, null);
					} catch (Exception e) {
						log("Failed to update bundle ", e);
					}
				} else {
					if (!isFragment(bundle)) {
						notStarts.add(bundle);
					}
				}
			} else {
				for (int j = 0; j < dirs.length; j++) {
					if (location.startsWith(dirs[j].getAbsolutePath())) {
						try {							
							bundle.uninstall();
							olds.remove(location);
							oldBundlesLastModify.remove(location);
							
							refresh = true;
							log("Uninstalled " + location, null);
						} catch (Exception e) {
							log("Failed to uninstall bundle: ", e);
						}
					}
				}
			}
		}

		for (File file : currs.values()) {
			try {
				InputStream in = new FileInputStream(file);
				Bundle bundle = context.installBundle(file.getAbsolutePath(), in);
				refresh = true;
				in.close();
                
				olds.put(bundle.getLocation(), file.length());
				oldBundlesLastModify.put(bundle.getLocation(), file.lastModified());
				
				if (!isFragment(bundle)) {
					starts.add(bundle);					
				}
				log("Installed " + file.getAbsolutePath(), null);
			} catch (Exception e) {
				log("Failed to install bundle:" + file.getAbsolutePath(), e);
			}
		}
        
		for (Bundle bundle : starts) {
			try {
				bundle.start();				
			} catch (BundleException e) {
				log("Fail to start bundle: " + bundle.getSymbolicName(), e);
			}
		}

		if (refresh) {
			refresh();
		}
	}
	
	/**
	 * @param string
	 * @param e
	 */
	private void log(String string, Throwable e) {
		System.err.println(string);
		if (e != null) {
			e.printStackTrace();
		}
	}
	
	/**
	 * @param string
	 * @param e
	 */
	private void log(String string) {
		log(string, null);
	}

	/**
	 * @param jars
	 * @param curConfigs
	 * @param jardir
	 */
	private void traverse(Map<String, File> jars, Map<String, File> configs, File jardir) {
		String list[] = jardir.list();
		for (int i = 0; i < list.length; i++) {
			File file = new File(jardir, list[i]);
			if (list[i].endsWith(".jar") || list[i].endsWith(".war")) {
				jars.put(file.getAbsolutePath(), file);
			} else if (list[i].endsWith(".cfg")) {
				configs.put(file.getAbsolutePath(), file);
			}
		}
	}

	/**
	 * @param bundle
	 * @return
	 */
	private boolean isFragment(Bundle bundle) {
		PackageAdmin padmin;
		if (BundleAutoDeploy.paTracker == null)
			return false;

		try {
			padmin = (PackageAdmin) BundleAutoDeploy.paTracker.waitForService(10000);
			if (padmin != null) {
				return padmin.getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
			}
		} catch (InterruptedException e) {
			// stupid exception
		}
		return false;
	}

	/**
	 * Refresh
	 */
	private void refresh() {
		PackageAdmin padmin;
		try {
			padmin = (PackageAdmin) BundleAutoDeploy.paTracker.waitForService(10000);
			padmin.refreshPackages(null);
		} catch (InterruptedException e) {
			// stupid exception
		}
	}

	/**
	 * Watcher close
	 */
	public void close() {
		polling = false;
		interrupt();
		try {
			join(10000); // 10s
		} catch (InterruptedException ie) {
			// Ignore
		}
	}
    
}
